package com.starview.star.compiler.format;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;

import com.starview.platform.data.type.Location;
import com.starview.platform.data.value.ResourceURI;
import com.starview.platform.resource.ResourceException;
import com.starview.platform.resource.Resources;
import com.starview.platform.resource.catalog.Catalog;
import com.starview.platform.resource.catalog.CatalogException;
import com.starview.platform.resource.catalog.URIBasedCatalog;
import com.starview.star.LanguageException;
import com.starview.star.StarCompiler;
import com.starview.star.StarMake;
import com.starview.star.StarRules;
import com.starview.star.code.repository.CodeRepository;
import com.starview.star.code.repository.CodeRepositoryImpl;
import com.starview.star.code.repository.RepositoryException;
import com.starview.star.code.repository.RepositoryManager;
import com.starview.star.compiler.ErrorReport;
import com.starview.star.compiler.ast.Apply;
import com.starview.star.compiler.ast.BigDecimalLiteral;
import com.starview.star.compiler.ast.BooleanLiteral;
import com.starview.star.compiler.ast.CharLiteral;
import com.starview.star.compiler.ast.DefaultAbstractVisitor;
import com.starview.star.compiler.ast.FloatLiteral;
import com.starview.star.compiler.ast.IAbstract;
import com.starview.star.compiler.ast.IAbstractVisitor;
import com.starview.star.compiler.ast.IAttribute;
import com.starview.star.compiler.ast.IntegerLiteral;
import com.starview.star.compiler.ast.LongLiteral;
import com.starview.star.compiler.ast.Name;
import com.starview.star.compiler.ast.StringLiteral;
import com.starview.star.compiler.format.rules.FmtProgram;
import com.starview.star.compiler.sources.MetaRules;
import com.starview.star.compiler.standard.StandardNames;
import com.starview.star.compiler.util.ApplicationProperties;
import com.starview.star.compiler.util.PrettyPrintDisplay;
import com.starview.star.compiler.util.PrettyPrintable;
import com.starview.star.compiler.wff.WffEngine;
import com.starview.star.compiler.wff.WffProgram;

/**
 * Implement source-code formatting of StarRules text based on rules
 * 
 * @author fgm
 * 
 */
public class Formatter
{
  private final LocationAttribute[] atts;
  private final String original;
  int ix;
  int currColumn = 0;
  boolean onlySpaces = true; // have we emitted any non-spaces on this line?
  boolean inLineComment = false;
  boolean inBlockComment = false;
  StringBuilder blder = new StringBuilder();
  final int oLength;

  public static final boolean TRACE = false;

  private Formatter(String original, LocationAttribute atts[])
  {
    this.original = original;
    this.atts = atts;
    this.oLength = original.length();
  }

  public static String formatSource(ResourceURI uri, ErrorReport errors, String category, CodeRepository repository,
      Catalog catalog) throws LanguageException, ResourceException, CatalogException, RepositoryException
  {
    StarMake.compile(repository, uri, catalog, errors);
    final MetaRules pkg = RepositoryManager.locateMeta(repository, uri);

    if (pkg != null) {
      // reparse the original
      IAbstract parse = StarCompiler.parse(uri, errors, repository);

      if (errors.isErrorFree()) {

        WffProgram wffRules = new WffProgram();
        FmtProgram fmtRules = new FmtProgram();

        MetaRules meta = RepositoryManager.locateMeta(repository, StarCompiler.starRulesURI);
        assert meta != null;

        wffRules.importRules(meta.getWffRules());
        fmtRules.importRules(meta.getFmtRules());

        WffEngine validator = new WffEngine(errors, wffRules);

        // Decorate tree with categories
        validator.validate(parse, StandardNames.WFF_STATEMENT);

        // Apply rules
        IAbstractVisitor formatVisitor = new FormatVisitor(fmtRules);
        parse.accept(formatVisitor);

        // Sort locations
        List<LocationAttribute> locs = getAllLocations(parse);

        LocationAttribute[] sorted = locs.toArray(new LocationAttribute[locs.size()]);

        Arrays.sort(sorted);

        adjustInheritance(sorted);

        Formatter formatter = new Formatter(Resources.getUriContent(uri), sorted);
        return formatter.format();

      } else
        throw new LanguageException(errors);
    } else
      return null;
  }

  private String format()
  {
    FormatPolicy policies = new FormatPolicy();
    Stack<FormatPolicy> policyStack = new Stack<FormatPolicy>();
    int attIx = 0;

    while (ix < oLength) {

      // Adjust policies based on entry to new region
      while (attIx < atts.length) {

        LocationAttribute locAtt = atts[attIx];
        Location loc = locAtt.loc;

        if (loc.getCharCount() <= ix) {
          if (loc.getCharCount() + loc.getLength() >= ix) {
            policyStack.push(policies);
            policies = policies.adjustPolicies(locAtt.loc, locAtt.attributes);
            if (policies.breakBefore)
              breakLine(policies);
            else
              adjustIndent(policies);
          }
          attIx++;
          continue;
        }
        break;
      }

      int curr = original.codePointAt(ix);
      ix = original.offsetByCodePoints(ix, 1);

      switch (curr) {
      case '\n':
        breakLine(policies);
        break;
      case '-': // Check for line comment
      {
        if (!inLineComment && !inBlockComment && policies.lineCommentColumnPolicy >= currColumn
            && (original.startsWith("- ", ix) || original.startsWith("-\t", ix))) {
          if (onlySpaces)
            adjustIndent(policies);
          else {
            int endPoint = blder.length() - 1;
            char ch = blder.charAt(endPoint);
            while (currColumn > 0 && ch == '\t' || ch == ' ') {
              blder.deleteCharAt(endPoint);
              endPoint--;
              ch = blder.charAt(endPoint);
              currColumn--;
            }

            while (currColumn < policies.lineCommentColumnPolicy) {
              blder.append(' ');
              currColumn++;
            }
          }
          inLineComment = true;
        }
        blder.appendCodePoint(curr);
        currColumn++;
        onlySpaces = false;
        break;
      }

      case '/': // Check for block comment
      {
        if (!inLineComment && !inBlockComment && policies.lineCommentColumnPolicy >= currColumn
            && original.startsWith("*", ix)) {
          inBlockComment = true;
        }
        blder.appendCodePoint(curr);
        currColumn++;
        onlySpaces = false;
        break;
      }

      case '*': // Check for end of block comment
        if (inBlockComment && ix < oLength && original.codePointAt(ix) == '/') {
          inBlockComment = false;
        }
        blder.appendCodePoint(curr);
        onlySpaces = false;
        currColumn++;
        break;

      default:
        blder.appendCodePoint(curr);
        if (!Character.isWhitespace(curr))
          onlySpaces = false;
        else if (inBlockComment && policies.commentWrap && currColumn >= policies.commentWrapColumn)
          breakLine(policies);
        currColumn++;
        break;
      }

      while (!policyStack.isEmpty() && policies.expiredLocation(ix)) {
        if (policies.breakAfter)
          breakLine(policyStack.peek());

        policies = policyStack.pop();
        adjustIndent(policies);
      }
    }
    return blder.toString();
  }

  private void adjustIndent(FormatPolicy policies)
  {
    if (onlySpaces && currColumn != policies.indentPolicy) {
      if (currColumn > policies.indentPolicy) {
        while (currColumn > policies.indentPolicy && blder.charAt(blder.length() - 1) == ' ') {
          currColumn--;
          blder.setLength(blder.length() - 1);
        }
      } else {
        while (currColumn < policies.indentPolicy) {
          blder.append(' ');
          currColumn++;
        }
      }
    }
  }

  private void breakLine(FormatPolicy policies)
  {
    inLineComment = false;
    onlySpaces = true;

    if (policies.blankLinePolicy >= 0) {
      int blanks = policies.blankLinePolicy;

      // Trim blank lines already generated
      char ch;
      int endPoint = blder.length();
      boolean doubleBreak = false;
      while (endPoint > 0 && Character.isWhitespace(ch = blder.charAt(endPoint - 1))) {
        if (ch == '\n')
          doubleBreak = true;

        endPoint--;
      }

      if (!doubleBreak) {
        currColumn = 0;

        while (ix < oLength && Character.isWhitespace(original.codePointAt(ix))) {
          if (original.codePointAt(ix) == '\n' && blanks > 0) {
            blder.append('\n');
            blanks--;
          }

          ix = original.offsetByCodePoints(ix, 1);
        }
        if (blanks >= 0)
          blder.append('\n');
      }
    } else {
      // Consume extra new lines and white space
      while (ix < oLength && Character.isWhitespace(original.codePointAt(ix)))
        ix = original.offsetByCodePoints(ix, 1);
    }

    if (ix < oLength) {
      for (; currColumn < policies.indentPolicy; currColumn++)
        blder.append(' ');
    }
  }

  private static List<LocationAttribute> getAllLocations(IAbstract tree)
  {
    final List<LocationAttribute> locs = new ArrayList<LocationAttribute>();

    tree.accept(new DefaultAbstractVisitor() {

      private void visit(IAbstract term)
      {
        final Map<String, IAttribute> attributes = term.getAttributes();

        if (attributes != null)
          locs.add(new LocationAttribute(term.getLoc(), attributes));
      }

      @Override
      public void visitApply(Apply term)
      {
        visit(term);
        super.visitApply(term);
      }

      @Override
      public void visitBooleanLiteral(BooleanLiteral lit)
      {
        visit(lit);
      }

      @Override
      public void visitCharLiteral(CharLiteral lit)
      {
        visit(lit);
      }

      @Override
      public void visitFloatLiteral(FloatLiteral lit)
      {
        visit(lit);
      }

      @Override
      public void visitIntegerLiteral(IntegerLiteral lit)
      {
        visit(lit);
      }

      @Override
      public void visitLongLiteral(LongLiteral lit)
      {
        visit(lit);
      }

      @Override
      public void visitName(Name name)
      {
        visit(name);
      }

      @Override
      public void visitBigDecimal(BigDecimalLiteral lit)
      {
        visit(lit);
      }

      @Override
      public void visitStringLiteral(StringLiteral str)
      {
        visit(str);
      }
    });
    return locs;
  }

  private static void adjustInheritance(LocationAttribute[] locs)
  {
    for (int ix = 0; ix < locs.length; ix++) {
      Map<String, IAttribute> atts = locs[ix].attributes;
      for (Entry<String, IAttribute> entry : atts.entrySet()) {
        IAttribute attribute = entry.getValue();
        if (!attribute.isIheritable() && attribute instanceof BooleanAttribute) {
          // For now, must be a BooleanAttribute
          IAttribute inverse = new BooleanAttribute(!((BooleanAttribute) attribute).apply(false),
              ((BooleanAttribute) attribute).getSpecificity());
          deherit(locs, ix + 1, locs[ix].loc, entry.getKey(), inverse);
        }
      }
    }
  }

  private static void deherit(LocationAttribute[] locs, int ix, Location loc, String att, IAttribute inverse)
  {
    while (ix < locs.length && within(loc, locs[ix].loc)) {
      if (!(locs[ix].attributes.containsKey(att)))
        locs[ix].attributes.put(att, inverse);

      ix++;
    }
  }

  private static boolean within(Location outer, Location inner)
  {
    return inner.getCharCount() >= outer.getCharCount()
        && inner.getCharCount() + inner.getLength() <= outer.getCharCount() + outer.getLength();
  }

  @SuppressWarnings("serial")
  private static class LocationAttribute implements Comparable<LocationAttribute>, PrettyPrintable
  {
    Location loc;
    Map<String, IAttribute> attributes;

    LocationAttribute(Location loc, Map<String, IAttribute> attributes)
    {
      this.loc = loc;
      this.attributes = attributes;
    }

    @Override
    public int compareTo(LocationAttribute o)
    {
      Location oLoc = o.loc;

      if (loc.getCharCount() < oLoc.getCharCount())
        return -1;
      else if (loc.getCharCount() > oLoc.getCharCount())
        return 1;
      else if (loc.getLength() > oLoc.getLength())
        return -1;
      else if (loc.getLength() < oLoc.getLength())
        return 1;
      else
        return 0;
    }

    @Override
    public String toString()
    {
      return PrettyPrintDisplay.toString(this);
    }

    @Override
    public void prettyPrint(PrettyPrintDisplay disp)
    {
      disp.append(loc.getLineCount());
      disp.append("[");
      disp.append(loc.getLineOffset());
      disp.append(":");
      disp.append(loc.getLength());
      disp.append("]");

      disp.append("-->");
      disp.append("{");
      int mark = disp.markIndent(2);
      String sep = "";
      for (Entry<String, IAttribute> entry : attributes.entrySet()) {
        disp.append(sep);
        sep = "; ";
        disp.append(entry.getKey());
        disp.append(StandardNames.COLON);
        entry.getValue().prettyPrint(disp);
      }
      disp.popIndent(mark);
      disp.append("}");
    }

  }

  public static void main(String args[])
  {
    ErrorReport errors = new ErrorReport();
    if (args.length > 0) {
      try {
        String nArgs[] = new String[args.length - 1];
        for (int ix = 0; ix < nArgs.length; ix++)
          nArgs[ix] = args[ix + 1];

        CodeRepository repository = new CodeRepositoryImpl(Thread.currentThread().getContextClassLoader(), true, errors);

        ResourceURI uri = ResourceURI.parseURI(args[0]);
        ResourceURI sourceURI = ApplicationProperties.wdURI.resolve(uri);

        Catalog catalog = new URIBasedCatalog(sourceURI, StarRules.starCatalog());
        String result = formatSource(uri, errors, StandardNames.PACKAGE, repository, catalog);

        System.out.println(result);
      } catch (Exception e) {
        System.err.println("Format errors in " + args[0] + "\n" + e.getMessage());
      }
    } else
      System.err.println("usage: <> sourceFile");
  }
}
